Skip to content

kvm: refresh the RBD pool when resizing a volume libvirt has not cached - #14623

Open
bhouse-nexthop wants to merge 3 commits into
apache:4.22from
bhouse-nexthop:rbd-resize-libvirt-cache
Open

bhouse-nexthop wants to merge 3 commits into
apache:4.22from
bhouse-nexthop:rbd-resize-libvirt-cache

Conversation

@bhouse-nexthop

Copy link
Copy Markdown
Collaborator

Description

Resizing a volume on RBD primary storage fails on KVM when the host's libvirt has not cached the image in its view of the pool. A freshly deployed VM's ROOT volume is the usual case:

org.libvirt.LibvirtException: Storage volume not found: no storage vol with matching path '<ceph pool>/<uuid>'

This is a regression from #14188.

Fixes #14622

Why it happens

LibvirtResizeVolumeCommandWrapper gets the disk with pool.getPhysicalDisk(), then looks the volume up through libvirt with conn.storageVolLookupByPath(path) to call StorageVol.resize().

Fix

When storageVolLookupByPath() fails with VIR_ERR_NO_STORAGE_VOL on an RBD pool, refresh that pool and look the volume up once more.

  • This is what getVolume() did for this path before kvm: look RBD volumes up through librbd instead of refreshing the whole pool #14188. The refresh only happens when the volume is not cached.
  • Any other libvirt error, and every other pool type, fails the resize as before, with no refresh.
  • Nothing else in the resize changes: the flags, StorageVol.resize(), then the resize script, which for RBD is NOTIFYONLY and runs virsh blockresize on a running VM.

I looked at two other fixes:

  • Resize RBD images through librbd and skip libvirt for RBD, as the rest of LibvirtStorageAdaptor already does for create, clone and delete. That avoids the refresh entirely, but it replaces the resize itself (shrink handling, the image open, error reporting), so it is a bigger change for a regression fix. It would make a reasonable follow-up.
  • Reuse LibvirtStorageAdaptor.getVolume(). It looks the volume up by name on the libvirt pool object and refreshes on any error, so it would change the lookup for volumes that are cached, too.

Other call sites

I checked the KVM plugin for other places that rely on getPhysicalDisk() having refreshed the libvirt pool. There are none:

  • LibvirtStorageAdaptor.deletePhysicalDisk() calls getVolume() itself, which still refreshes on a miss.
  • LibvirtMigrateCommandWrapper.deleteLocalVolume() uses storageVolLookupByPath(), but only for local files.

Not changed here

execute() already calls pool.refresh() after every resize, before it reads the new size back. For RBD that refresh no longer affects the result, since the size now comes from librbd. On a cache miss, this PR adds a second refresh. Dropping the trailing one for RBD is a separate change.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • build/CI
  • test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

How Has This Been Tested?

Unit tests. A live lab retest will follow.

LibvirtComputingResourceTest passes (273 tests, checkstyle on). The new tests are:

  • testResizeVolumeCommandRbdVolumeNotInLibvirtPoolCache: the first lookup throws not found on an RBD pool. The resize refreshes the pool, finds the volume, resizes it, then runs the notify-only script, and answers success with the new size.
  • testResizeVolumeCommandRbdVolumeNotFoundAfterRefresh and testResizeVolumeCommandRbdVolumeRetriedAfterFailedRefresh: the volume is still missing after the refresh, whether or not the refresh itself worked. There is exactly one refresh and one retry, then the resize fails.
  • testResizeVolumeCommandRbdOtherLookupErrorDoesNotRefresh and testResizeVolumeCommandRbdLookupErrorWithoutCodeDoesNotRefresh: any other lookup error on RBD fails without a refresh.
  • testResizeVolumeCommandNonRbdVolumeNotFoundDoesNotRefresh: a missing volume on NFS fails without a refresh, as before.

Without the fix, the first three of these fail. The other three pass with or without it; they check that nothing else changed.

LibvirtResizeVolumeCommandWrapper gets the disk with
pool.getPhysicalDisk() and then looks the volume up through libvirt
with storageVolLookupByPath() to resize it.

Before apache#14188, getPhysicalDisk() went through
LibvirtStorageAdaptor.getVolume(), which refreshes the libvirt pool when
the volume is not found, so the volume was in libvirt's cache by the
time the wrapper looked it up. apache#14188 made getPhysicalDisk() stat RBD
images through librbd, so nothing refreshes the pool any more. RBD
volumes are created through librbd, not libvirt, so a new volume is not
in libvirt's cache on a host until something refreshes the pool there,
and resizing it fails with:

  Storage volume not found: no storage vol with matching path
  '<pool>/<uuid>'

When the lookup by path fails with VIR_ERR_NO_STORAGE_VOL on an RBD
pool, refresh the pool and look the volume up once more. That is what
getVolume() did for this path before apache#14188, and the refresh only
happens when the volume is not cached. Other errors and other pool
types are handled as before.

Signed-off-by: Brad House <bhouse@nexthop.ai>
@codecov

codecov Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 18.32%. Comparing base (d02adcd) to head (d18b117).
⚠️ Report is 1 commits behind head on 4.22.

Additional details and impacted files
@@            Coverage Diff            @@
##               4.22   #14623   +/-   ##
=========================================
  Coverage     18.32%   18.32%           
- Complexity    16644    16653    +9     
=========================================
  Files          5939     5939           
  Lines        537768   537799   +31     
  Branches      65929    65935    +6     
=========================================
+ Hits          98551    98571   +20     
- Misses       428008   428019   +11     
  Partials      11209    11209           
Flag Coverage Δ
uitests 4.08% <ø> (ø)
unittests 19.42% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bhouse-nexthop

Copy link
Copy Markdown
Collaborator Author

Test report: resizing RBD volumes that libvirt has not cached, with no manual pool refresh

Environment

1. Size-only resize of a fresh VM's ROOT, VM running

  • Before: the ROOT image was not in virsh vol-list on the host.
  • resizeVolume size=12 succeeded in 4 s. The API, virsh domblkinfo, rbd info and the guest's lsblk all showed 12 GiB, with no reboot. The VM stayed Running.
  • The agent logged the refresh once:
    INFO  [resource.wrapper.LibvirtResizeVolumeCommandWrapper] Volume cloudstack/<uuid> is not in libvirt's cache of RBD pool <pool>
          (Storage volume not found: no storage vol with matching path 'cloudstack/<uuid>'). Refreshing the pool and looking the volume up again.
    
  • After: the image was in virsh vol-list.
  • A second resize of the same volume (size=14) succeeded with no refresh line, because the lookup hit the cache. So the refresh happens only on a miss. All four views showed 14 GiB.

On the build without this PR, the same first resize failed with Storage volume not found: no storage vol with matching path 'cloudstack/<uuid>' (see #14622). That is the failure this PR fixes.

2. Data volume, created, attached and resized while attached to the running VM

  • A 5 GB volume on the Custom offering, attached to the VM from step 1. The guest saw it as vdb.
  • resizeVolume size=8 succeeded in 5 s. API, libvirt, RBD and the guest's lsblk /dev/vdb all showed 8 GiB.
  • On this host the image was already in libvirt's cache before the resize, so the resize found it on the first lookup and logged no refresh. The pool refresh from step 1 ran before this volume existed, so the attach must have put it in the cache. This step shows the normal path still works, and that the retry stays out of the way when it isn't needed.

3. resizeVolume diskofferingid=root20 on a fresh running VM (the #14602 path)

  • A second fresh VM, on another host. Its ROOT was not in virsh vol-list there.
  • resizeVolume diskofferingid=root20 succeeded in 5 s, and the volume is now on root20 at 20 GiB in all four views. The VM stayed Running.
  • The agent logged the same single refresh-and-retry line.
  • In the guest, growpart and resize2fs grew / to 19G.

Result: pass.

  • A resize of an RBD volume that is not in libvirt's pool cache now refreshes the pool once and succeeds, for a size-only ROOT resize, a data volume, and a resize through a new disk offering.
  • No manual virsh pool-refresh was needed. When the volume is already cached, the resize doesn't refresh.

Not covered

  • A refresh that fails (the warning path), and a volume that is still missing after the refresh. Both are covered by the unit tests.
  • Encrypted RBD volumes, and non-RBD pools. Neither goes through the retry.

Move the lookup by path that refreshes an RBD pool when the volume is
not in libvirt's cache out of LibvirtResizeVolumeCommandWrapper and into
LibvirtUtilitiesHelper.getStorageVolumeByPath(), and call it from the
wrapper. The behaviour and the log messages are unchanged.

The tests of the lookup itself move to LibvirtUtilitiesHelperTest.
LibvirtComputingResourceTest keeps a resize of an RBD volume that is
not in libvirt's cache, through the real helper, and adds a resize that
fails when the lookup fails.

Signed-off-by: Brad House <bhouse@nexthop.ai>

@weizhouapache weizhouapache left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code lgtm

not tested yet

@weizhouapache

Copy link
Copy Markdown
Member

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@weizhouapache a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 19957

@weizhouapache

Copy link
Copy Markdown
Member

@blueorangutan test

@blueorangutan

Copy link
Copy Markdown

@weizhouapache a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests

@weizhouapache

Copy link
Copy Markdown
Member

I ran this change on a KVM + Ceph RBD test environment (two KVM hosts, two RBD primary storage pools) to check whether the libvirt cache-miss path behaves as the PR describes.

Test change. The existing smoke tests test_08_resize_volume and test_12_resize_volume_with_only_size_parameter only resized DATADISK volumes. I added a step to both tests that grows the VM's ROOT volume by 1 GB while the VM keeps running, and checks that the volume reaches the new size and is Ready. The step has no storage-type filter, so it runs on any primary storage.

Diff of the test change (test/integration/smoke/test_volumes.py)
@@ -746,6 +746,8 @@ class TestVolumes(cloudstackTestCase):
                 "Check if the root volume resized appropriately"
             )
 
+        self.resize_root_volume()
+
         # start the vm if it is on xenserver
 
         if hosts[0].hypervisor == "XenServer":
@@ -977,6 +979,8 @@ class TestVolumes(cloudstackTestCase):
             "Check if the data volume resized appropriately"
         )
 
+        self.resize_root_volume()
+
         # start the vm if it is on xenserver
 
         if hosts[0].hypervisor == "XenServer":
@@ -984,6 +988,28 @@ class TestVolumes(cloudstackTestCase):
             time.sleep(30)
         return
 
+    def resize_root_volume(self):
+        """Grow the ROOT volume of the VM by one GB, on any primary storage type."""
+        root = self.wait_for_attributes_and_return_root_vol()
+        new_size_gb = int(math.ceil(float(root.size) / (1024 ** 3))) + 1
+        self.debug("Resize ROOT volume (ID: %s) to %d GB" % (root.id, new_size_gb))
+
+        cmd = resizeVolume.resizeVolumeCmd()
+        cmd.id = root.id
+        cmd.size = new_size_gb
+        self.apiClient.resizeVolume(cmd)
+
+        def root_resized():
+            vols = Volume.list(self.apiClient, id=root.id, listall=True)
+            if isinstance(vols, list) and len(vols) > 0:
+                vol = vols[0]
+                if int(vol.size) == new_size_gb * (1024 ** 3) and vol.state == 'Ready':
+                    return True, vol
+            return False, None
+
+        res, _ = wait_until(5, 36, root_resized)
+        self.assertTrue(res, "Check if the ROOT volume resized appropriately")
+
     def wait_for_attributes_and_return_root_vol(self):
         def checkVolumeResponse():

Reproduction. I created a data volume, attached it to a running VM on one host (which makes the agent create the RBD image through librbd), then resized it right away. Right after the attach, virsh vol-info on that host returned Storage volume not found, so libvirt's cache did not have the image. On the unpatched agent, the resize then failed with the Storage volume not found error from this PR.

Results.

  • Unpatched agent, 6 runs of test_08 and test_12: 3 runs failed in the ROOT-volume resize with this error. One more run failed earlier, in a test helper, before the resize.
  • Patched agent, same repro, 5 runs: all 5 resizes succeeded. In 3 of them the agent logged that the volume was missing from libvirt's cache, refreshed the pool, and found the volume on the retry.

Notes. The refresh runs on the pool lookup, so it is limited to the RBD pool and the one lookup that misses. The unpatched runs show that the refresh is needed, and the patched runs show that it recovers from the miss. I have not tested a non-RBD pool, a volume that is already in libvirt's cache, or a shrink.

@weizhouapache

Copy link
Copy Markdown
Member

@bhouse-nexthop
can you update test/integration/smoke/test_volumes.py to cover the resize of ROOT volumes ?
see my comment above

weizhouapache added a commit to weizhouapache/cloudstack that referenced this pull request Nov 10, 2026
…ed (PR 14623)

Cherry-pick of the PR apache#14623 changes to the resize path, applied on
4.23.0-ceph. The VM snapshot XML tests from the PR are left out since
they are not on this branch.
test_08_resize_volume and test_12_resize_volume_with_only_size_parameter
only resized DATADISK volumes. Both now also grow the VM's ROOT volume
by 1 GB while it runs and wait for the new size and the Ready state. The
step runs on any primary storage type, so on Ceph RBD it covers the
libvirt pool-cache miss this PR fixes. PowerFlex rounds sizes up to a
multiple of 8 GB, which the expected size accounts for, as in test_12.

Suggested by Wei Zhou.

Signed-off-by: Brad House <bhouse@nexthop.ai>
@boring-cyborg boring-cyborg Bot added component:integration-test Python Warning... Python code Ahead! labels Nov 10, 2026
@bhouse-nexthop

Copy link
Copy Markdown
Collaborator Author

@weizhouapache thanks for testing this on RBD, and for the repro numbers. Done in d18b117: test_08_resize_volume and test_12_resize_volume_with_only_size_parameter now both grow the running VM's ROOT volume by 1 GB, using your resize_root_volume() step, on any primary storage type.

One change from your diff: PowerFlex rounds volume sizes up to a multiple of 8 GB, so the expected size is rounded the same way there, as test_12 already does for the data disk. Otherwise a +1 GB resize on PowerFlex would never match.

@blueorangutan

Copy link
Copy Markdown

[SF] Trillian test result (tid-17530)
Environment: kvm-ol8 (x2), zone: Advanced Networking with Mgmt server ol8
Total time taken: 59824 seconds
Marvin logs: https://github.com/blueorangutan/acs-prs/releases/download/trillian/pr14623-t17530-kvm-ol8.zip
Smoke tests completed. 149 look OK, 0 have errors, 0 did not run
Only failed and skipped tests results shown below:

Test Result Time (s) Test File

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants